Coverage Report

Created: 2026-08-07 16:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\cssh-rs\cssh-rs\cssh-rs-platform-windows\src\traits.rs
Line
Count
Source
1
//! Windows implementations of the `cssh-rs-platform` traits.
2
//!
3
//! The impls wrap the existing [`WindowsApi`] helpers and
4
//! `tokio::net::windows::named_pipe` so the daemon and client can be
5
//! ported to the platform-agnostic surface in later milestones. They are
6
//! not consumed by the M0 daemon/client yet; the trait surface is what
7
//! Linux and macOS stubs need to mirror.
8
//!
9
//! [`WindowsApi`]: crate::api::WindowsApi
10
11
use std::ffi::{OsStr, OsString};
12
use std::io;
13
use std::sync::Arc;
14
15
use cssh_rs_platform::{
16
    ControlChannelClient, ControlChannelServer, LaunchContext, ProcessSpawner, WindowHandleProbe,
17
};
18
use tokio::io::{AsyncReadExt, AsyncWriteExt, Interest};
19
use tokio::net::windows::named_pipe::{
20
    ClientOptions, NamedPipeClient, NamedPipeServer, PipeMode, ServerOptions,
21
};
22
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND};
23
use windows::Win32::System::Threading::PROCESS_INFORMATION;
24
25
use crate::api::{DefaultWindowsApi, WindowsApi};
26
27
/// `Send + Sync` wrapper around [`PROCESS_INFORMATION`].
28
///
29
/// [`PROCESS_INFORMATION`] is `!Send` because its `HANDLE` fields wrap raw
30
/// pointers. The handles are owned by the spawning process and remain valid
31
/// across thread boundaries, so the wrapper asserts thread safety with
32
/// `unsafe impl`. The daemon already uses the same pattern for
33
/// `Client::process_handle` via its private `HWNDWrapper`.
34
#[derive(Debug)]
35
pub struct SendProcessInformation(pub PROCESS_INFORMATION);
36
37
// SAFETY: PROCESS_INFORMATION's HANDLE fields are opaque kernel handles,
38
// not pointers into the calling thread's address space; the OS allows the
39
// owning process to use them from any thread.
40
unsafe impl Send for SendProcessInformation {}
41
// SAFETY: same justification as for `Send` - the handle values are immutable
42
// once `CreateProcessW` returns, so shared references can travel freely.
43
unsafe impl Sync for SendProcessInformation {}
44
45
impl Drop for SendProcessInformation {
46
3
    fn drop(&mut self) {
47
        // CreateProcessW returns kernel handles to the new process and its
48
        // primary thread; both must be closed to avoid leaking entries from
49
        // the per-process handle table. Skip NULL/invalid handles to avoid
50
        // the ERROR_INVALID_HANDLE that CloseHandle reports for them - see
51
        // https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle
52
6
        for handle in 
[self.0.hProcess, self.0.hThread]3
{
53
6
            if !handle.is_invalid() {
54
0
                // SAFETY: handle came from CreateProcessW (or a test-side
55
0
                // fake) and is not aliased anywhere else - the wrapper owns
56
0
                // it for its entire lifetime.
57
0
                let _ = unsafe { CloseHandle(handle) };
58
6
            }
59
        }
60
3
        self.0.hProcess = HANDLE::default();
61
3
        self.0.hThread = HANDLE::default();
62
3
    }
63
}
64
65
/// `Send + Sync` wrapper around [`HWND`].
66
///
67
/// Mirrors the daemon's private `HWNDWrapper` so the platform-trait
68
/// surface can be `Send + Sync` without consumers having to define their
69
/// own newtype.
70
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71
pub struct SendHwnd(pub HWND);
72
73
// SAFETY: HWND is an opaque user-mode kernel object identifier the OS lets
74
// any thread in the owning process consult.
75
unsafe impl Send for SendHwnd {}
76
// SAFETY: same justification as for `Send` - the underlying identifier is
77
// immutable and read-only from Rust's perspective.
78
unsafe impl Sync for SendHwnd {}
79
80
/// Windows-side [`LaunchContext`].
81
///
82
/// Carries the focus-handoff hint the daemon uses when spawning client
83
/// consoles. The current Win32 spawn path inspects the same flag to apply
84
/// `STARTF_USESHOWWINDOW` + `SW_SHOWNOACTIVATE`, so this field is the
85
/// minimum needed to round-trip today's behaviour through the trait.
86
#[derive(Debug, Clone, Copy, Default)]
87
pub struct WindowsLaunchContext {
88
    /// Whether the spawned console is allowed to take foreground focus.
89
    pub with_keyboard_focus: bool,
90
}
91
92
impl LaunchContext for WindowsLaunchContext {}
93
94
/// Process spawner backed by [`WindowsApi::create_process_with_os_args`].
95
///
96
/// Holds an [`Arc`] over a [`WindowsApi`] implementation so callers can
97
/// inject a mock in tests (the crate's `mock` feature exposes
98
/// `MockWindowsApi` for this purpose); production code uses
99
/// [`Self::default`] which selects [`DefaultWindowsApi`].
100
pub struct WindowsProcessSpawner<A: WindowsApi = DefaultWindowsApi> {
101
    api: Arc<A>,
102
}
103
104
impl<A: WindowsApi> WindowsProcessSpawner<A> {
105
    /// Construct a spawner around a specific [`WindowsApi`] implementation.
106
    ///
107
    /// # Arguments
108
    /// * `api` - Windows API operations implementation.
109
4
    pub fn new(api: Arc<A>) -> Self {
110
4
        return Self { api };
111
4
    }
112
}
113
114
impl Default for WindowsProcessSpawner<DefaultWindowsApi> {
115
1
    fn default() -> Self {
116
1
        return Self::new(Arc::new(DefaultWindowsApi));
117
1
    }
118
}
119
120
impl<A: WindowsApi + 'static> ProcessSpawner for WindowsProcessSpawner<A> {
121
    type Context = WindowsLaunchContext;
122
    type Handle = SendProcessInformation;
123
    type Error = io::Error;
124
125
3
    fn spawn(
126
3
        &self,
127
3
        program: &OsStr,
128
3
        args: &[OsString],
129
3
        context: &Self::Context,
130
3
    ) -> Result<Self::Handle, Self::Error> {
131
3
        return self
132
3
            .api
133
3
            .create_process_with_os_args(program, args, context.with_keyboard_focus)
134
3
            .map(SendProcessInformation)
135
3
            .map_err(|err| 
{1
136
                // The thread-local last-error code is what CreateProcessW
137
                // actually reports; the windows-rs `Error` typically just
138
                // re-wraps it but stringifies it as an opaque HRESULT.
139
1
                let os_err = io::Error::last_os_error();
140
1
                if os_err.raw_os_error().unwrap_or(0) != 0 {
141
1
                    return os_err;
142
0
                }
143
0
                return io::Error::other(format!("CreateProcessW failed: {err}"));
144
1
            });
145
3
    }
146
}
147
148
/// Daemon-side control channel backed by a Windows named pipe server.
149
///
150
/// One instance owns one [`NamedPipeServer`] endpoint. `accept`
151
/// waits for the matching client process to connect; `send` / `recv`
152
/// then exchange already-framed bytes produced by `cssh-rs-protocol`.
153
pub struct WindowsControlChannelServer {
154
    pipe: NamedPipeServer,
155
    endpoint: OsString,
156
}
157
158
impl WindowsControlChannelServer {
159
    /// Open a new named-pipe server endpoint at `endpoint`.
160
    ///
161
    /// # Arguments
162
    /// * `endpoint` - Fully-qualified Windows named-pipe name
163
    ///   (e.g. `\\.\pipe\cssh-rs-named-pipe-for-ipc`).
164
    ///
165
    /// # Returns
166
    /// Ready-to-`accept` server, or the underlying I/O error from
167
    /// [`ServerOptions::create`].
168
1
    pub fn bind(endpoint: &OsStr) -> io::Result<Self> {
169
1
        let pipe = ServerOptions::new()
170
1
            .access_inbound(true)
171
1
            .access_outbound(true)
172
1
            .pipe_mode(PipeMode::Message)
173
1
            .create(endpoint)
?0
;
174
1
        return Ok(Self {
175
1
            pipe,
176
1
            endpoint: endpoint.to_owned(),
177
1
        });
178
1
    }
179
180
    /// Endpoint name the server is bound to.
181
    ///
182
    /// # Returns
183
    /// The named-pipe path supplied to [`Self::bind`].
184
1
    pub fn endpoint(&self) -> &OsStr {
185
1
        return self.endpoint.as_os_str();
186
1
    }
187
188
    /// Test for pending readable bytes without blocking.
189
    ///
190
    /// # Returns
191
    /// `Ok(true)` when the pipe has bytes ready to read, `Ok(false)`
192
    /// otherwise, or the underlying I/O error.
193
1
    pub async fn ready_to_read(&mut self) -> io::Result<bool> {
194
1
        let ready = self.pipe.ready(Interest::READABLE).await
?0
;
195
1
        return Ok(ready.is_readable());
196
1
    }
197
}
198
199
impl ControlChannelServer for WindowsControlChannelServer {
200
    type Error = io::Error;
201
202
1
    async fn accept(&mut self) -> Result<(), Self::Error> {
203
1
        return self.pipe.connect().await;
204
1
    }
205
206
1
    async fn send(&mut self, frame: &[u8]) -> Result<(), Self::Error> {
207
1
        return self.pipe.write_all(frame).await;
208
1
    }
209
210
1
    async fn recv(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
211
1
        return self.pipe.read(buf).await;
212
1
    }
213
}
214
215
/// Client-side control channel backed by a Windows named pipe client.
216
pub struct WindowsControlChannelClient {
217
    pipe: Option<NamedPipeClient>,
218
}
219
220
impl WindowsControlChannelClient {
221
    /// Construct an unconnected control-channel client.
222
    ///
223
    /// # Returns
224
    /// A client whose `connect` has not yet run.
225
2
    pub fn new() -> Self {
226
2
        return Self { pipe: None };
227
2
    }
228
}
229
230
impl Default for WindowsControlChannelClient {
231
1
    fn default() -> Self {
232
1
        return Self::new();
233
1
    }
234
}
235
236
impl ControlChannelClient for WindowsControlChannelClient {
237
    type Error = io::Error;
238
239
1
    async fn connect(&mut self, endpoint: &OsStr) -> Result<(), Self::Error> {
240
1
        let pipe = ClientOptions::new().open(endpoint)
?0
;
241
1
        self.pipe = Some(pipe);
242
1
        return Ok(());
243
1
    }
244
245
2
    async fn send(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
246
2
        let 
pipe1
= self.pipe.as_mut().ok_or_else(||
{1
247
1
            return io::Error::other("control channel client is not connected");
248
1
        })?;
249
1
        return pipe.write_all(bytes).await;
250
2
    }
251
252
2
    async fn recv(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
253
2
        let 
pipe1
= self.pipe.as_mut().ok_or_else(||
{1
254
1
            return io::Error::other("control channel client is not connected");
255
1
        })?;
256
1
        return pipe.read(buf).await;
257
2
    }
258
}
259
260
/// Window-handle probe backed by [`WindowsApi::get_window_handle_for_process`].
261
///
262
/// Holds an [`Arc`] over a [`WindowsApi`] for the same testability reasons
263
/// as [`WindowsProcessSpawner`].
264
pub struct WindowsWindowHandleProbe<A: WindowsApi = DefaultWindowsApi> {
265
    api: Arc<A>,
266
}
267
268
impl<A: WindowsApi> WindowsWindowHandleProbe<A> {
269
    /// Construct a probe around a specific [`WindowsApi`] implementation.
270
    ///
271
    /// # Arguments
272
    /// * `api` - Windows API operations implementation.
273
3
    pub fn new(api: Arc<A>) -> Self {
274
3
        return Self { api };
275
3
    }
276
}
277
278
impl Default for WindowsWindowHandleProbe<DefaultWindowsApi> {
279
1
    fn default() -> Self {
280
1
        return Self::new(Arc::new(DefaultWindowsApi));
281
1
    }
282
}
283
284
impl<A: WindowsApi + 'static> WindowHandleProbe for WindowsWindowHandleProbe<A> {
285
    type Handle = SendHwnd;
286
287
2
    fn window_handle_for_process(&self, pid: u32) -> Option<Self::Handle> {
288
2
        let handle = self.api.get_window_handle_for_process(pid);
289
2
        if self.api.is_window(handle) {
290
1
            return Some(SendHwnd(handle));
291
1
        }
292
1
        return None;
293
2
    }
294
}
295
296
#[cfg(test)]
297
#[path = "tests/test_traits.rs"]
298
mod test_mod;